/*
 * Copyright (c) 1997-1999 The Java Apache Project.  All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in
 *    the documentation and/or other materials provided with the
 *    distribution.
 *
 * 3. All advertising materials mentioning features or use of this
 *    software must display the following acknowledgment:
 *    "This product includes software developed by the Java Apache 
 *    Project for use in the Apache JServ servlet engine project
 *    <http://java.apache.org/>."
 *
 * 4. The names "Apache JServ", "Apache JServ Servlet Engine" and 
 *    "Java Apache Project" must not be used to endorse or promote products 
 *    derived from this software without prior written permission.
 *
 * 5. Products derived from this software may not be called "Apache JServ"
 *    nor may "Apache" nor "Apache JServ" appear in their names without 
 *    prior written permission of the Java Apache Project.
 *
 * 6. Redistributions of any form whatsoever must retain the following
 *    acknowledgment:
 *    "This product includes software developed by the Java Apache 
 *    Project for use in the Apache JServ servlet engine project
 *    <http://java.apache.org/>."
 *    
 * THIS SOFTWARE IS PROVIDED BY THE JAVA APACHE PROJECT "AS IS" AND ANY
 * EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE JAVA APACHE PROJECT OR
 * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
 * OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * This software consists of voluntary contributions made by many
 * individuals on behalf of the Java Apache Group. For more information
 * on the Java Apache Project and the Apache JServ Servlet Engine project,
 * please see <http://java.apache.org/>.
 *
 */

package nc.tb.pub.util.script;

import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Vector;

import nc.tb.pub.NtbEnv;
import nc.tb.pub.data.ScriptDataManager;
import nc.tb.pub.util.NtbLogger;


/**
 * A class loader that loads classes from directories and/or zip-format file
 * such as JAR file. It tracks the modification time of the classes it loads to
 * permit reloading through re-instantiation.
 * <P>
 * When the classloader reports its creator that one of the classes it has
 * loaded has changed on disk, it should discard the classloader and create a
 * new instance using <CODE>reinstantiate</CODE>. The classes are then
 * reloaded into the new classloader as required.
 * 
 * <P>
 * The classloader can also load resources, which are a means for packaging
 * application data such as images within a jar file or directory.
 * 
 * <P>
 * The classloader always first tries to load classes and resources from the
 * system, and uses it's own path if that fails. This is also done if an empty
 * repository is passed at construction.
 * 
 * <P>
 * <B>How autoreload works:</B>
 * </P>
 * 
 * <P>
 * The Java VM considers two classes the same if they have the same
 * fully-qualified name <B>and</B> if they were loaded from the same <CODE>ClassLoader</CODE>.
 * 
 * <P>
 * There is no way for a classloader to 'undefine' a class once it has been
 * loaded. However, the servlet engine can discard a classloader and the classes
 * it contains, causing the
 * 
 * <P>
 * The <CODE>JServServletManager</CODE> creates a new instance of the
 * classloader each time it detects that any of the loaded classes have changed.
 * 
 * <P>
 * Before terminating, all servlets are destroyed.
 * 
 * <P>
 * According to the Java Language Specification (JLS), classes may be
 * garbage-collected when there are no longer any instances of that class and
 * the <CODE>java.lang.Class</CODE> object is finalizable. It is intended that
 * this be the case when a <CODE>JServClassLoader</CODE> is discarded.
 * 
 * <P>
 * Many VM releases did not implement class garbage collection properly. In such
 * a VM, the memory usage will continue to grow if autoreloading is enable.
 * Running the VM with <CODE>-verbosegc</CODE> (or the corresponding option
 * for non-Javasoft VMs) may give some debugging information.
 * 
 * <P>
 * It is important that the <CODE>destroy</CODE> method be implemented
 * properly, as servlets may be destroyed and reinitialized several times in the
 * life of a VM.
 * 
 * @author Francis J. Lacoste
 * @author Martin Pool
 * @author Jim Heintz
 * @author <a href="mailto:stefano@apache.org">Stefano Mazzocchi</a>
 * @version $Revision: 1.9.2.2 $ $Date: 2000/11/16 22:04:42 $
 * @see java.lang.ClassLoader
 */
public class NtbSecureScriptLoader extends ClassLoader {
	private static final boolean debug = false;

	/**
	 * Instance of the SecurityManager installed.
	 */
	static protected SecurityManager sm;

	/**
	 * Generation counter, incremented for each classloader as they are created.
	 */
	static private int generationCounter = 0;

	/**
	 * Generation number of the classloader, used to distinguish between
	 * different instances.
	 */
	private int generation;

	/**
	 * Cache of the loaded classes. This contains ClassCacheEntry keyed by class
	 * names.
	 */
	protected Hashtable cache;

	/**
	 * The classpath which this classloader searches for class definitions. Each
	 * element of the vector should be either a directory, a .zip file, or a
	 * .jar file.
	 * <p>
	 * It may be empty when only system classes are controlled.
	 */
	protected Vector repository;

	/**
	 * A parent class loader for delegation of finding a class definition. JDK
	 * 1.2 contains parent class loaders as part of java.lang.ClassLoader, the
	 * parent being passed to a constructor, and retreived with getParent()
	 * method. For JDK 1.1 compatibility, we'll duplicate the 1.2 private member
	 * var.
	 */
	protected ClassLoader parent;

	protected ScriptDataManager dataManager = ScriptDataManager.getInstance();
	/**
	 * Private class used to maintain information about the classes that we
	 * loaded.
	 */
	private static class ClassCacheEntry {

		/**
		 * The actual loaded class
		 */
		Class loadedClass;

		/**
		 * The primary of NtbScript from which this class was loaded; or null if
		 * it was loaded from the system.
		 */
		String className;

		/**
		 * The time at which the class was loaded from the origin file, in ms
		 * since the epoch.
		 */
		long lastModified;

		/**
		 * Check whether this class was loaded from the system.
		 */
		public boolean isSystemClass() {
			return className == null;
		}
	}

	// ------------------------------------------------------- Constructors

	/**
	 * Creates a new class loader that will load classes from specified class
	 * repositories, delegating first to the passed parent for definitions.
	 * 
	 * @param classRepository
	 *            An set of File classes indicating directories and/or zip/jar
	 *            files. It may be empty when only system classes are loaded.
	 * @param theParent
	 *            A containing class loader for initial delegation of class
	 *            definition serach.
	 * @throw java.lang.IllegalArgumentException if the objects contained in the
	 *        vector are not a file instance or the file is not a valid
	 *        directory or a zip/jar file.
	 */
	public NtbSecureScriptLoader() {
		// Create the cache of loaded classes
		cache = new Hashtable();
	}

	public void setRepository(Vector classRepository)
			throws IllegalArgumentException {
		// Verify that all the repository are valid.
		if(classRepository == null || classRepository.isEmpty())
			return ;
		Enumeration e = classRepository.elements();
		while (e.hasMoreElements()) {
			ClassRepository cp = (ClassRepository) e.nextElement();
			String strDataSource;

			// Check to see if element is a valid primary key.
			strDataSource = cp.getDataSource();
			if(strDataSource == null || strDataSource.trim().length() == 0)
				throw new IllegalArgumentException("Object " + cp
						+ "is not a valid \"Data Source\"!");
		}

		// Store the class repository for use
		this.repository = classRepository;

		// Increment and store generation counter
		this.generation = generationCounter++;
	}

	public void setParent(ClassLoader p) {
		parent = p;
	}

	void log(String s) {
		NtbLogger.print("NtbSecureScriptLoader: " + s);
	}

	/**
	 * Check to see if a given class should be reloaded because of a
	 * modification to the original class.
	 * 
	 * @param className
	 *            The name of the class to check for modification.
	 */
	public synchronized boolean shouldReload(String classname,long ts) {
		return checkExpired(classname,ts);
	}

	public boolean checkExpired(String classname,long ts) {
		ClassCacheEntry entry = (ClassCacheEntry) cache.get(classname);

		if (entry == null) {
			// class wasn't even loaded
			return false;
		} else if (entry.isSystemClass()) {
			// System classes cannot be reloaded
			return false;
		} else {
			return (ts != entry.lastModified);
		}
	}

	/**
	 * Check whether the classloader should be reinstantiated.
	 * <P>
	 * The classloader must be replaced if there is any class whose origin file
	 * has changed since it was last loaded.
	 */
	public synchronized boolean shouldReload() {

		// Check whether any class has changed
		Enumeration e = cache.elements();
		NtbScript aScript = null;
		while (e.hasMoreElements()) {
			ClassCacheEntry entry = (ClassCacheEntry) e.nextElement();

			if (entry.loadedClass == null)
				continue;
//			if (debug > 5)
			log("cache entry: " + entry.loadedClass.getName());
			if (entry.isSystemClass())
				continue;

			// XXX: Because we want the classloader to be an accurate
			// reflection of the contents of the repository, we also
			// reload if a class origin file is now missing. This
			// probably makes things a bit more fragile, but is OK in
			// a servlet development situation. <mbp@pharos.com.au>
			try {
				aScript = dataManager.getScriptByPK(entry.className);
				
				if (aScript == null) {
					// class no longer exists
					return true;
				}
				long msOrigin = aScript.getTs().getMillis();
				if (msOrigin != entry.lastModified) {
					// class is modified
					return true;
				}
			} catch (Exception ex) {
				NtbLogger.error(ex);
			}			
		}

		// No changes, no need to reload
		return false;
	}

	/**
	 * Re-instantiate this class loader.
	 * <p>
	 * This method creates a new instance of the class loader that will load
	 * classes form the same path as this one.
	 */
	public NtbSecureScriptLoader reinstantiate() {
		NtbSecureScriptLoader cl = new NtbSecureScriptLoader();
		cl.setParent(parent);
		cl.setRepository(repository);
		return cl;
	}

	// ------------------------------------ Implementation of Classloader

	/*
	 * XXX: The javadoc for java.lang.ClassLoader says that the ClassLoader
	 * should cache classes so that it can handle repeated requests for the same
	 * class. On the other hand, the JLS seems to imply that each classloader is
	 * only asked to load each class once. Is this a contradiction?
	 * 
	 * Perhaps the second call only applies to classes which have been
	 * garbage-collected?
	 */

	/**
	 * Resolves the specified name to a Class. The method loadClass() is called
	 * by the virtual machine. As an abstract method, loadClass() must be
	 * defined in a subclass of ClassLoader.
	 * 
	 * @param name
	 *            the name of the desired Class.
	 * @param resolve
	 *            true if the Class needs to be resolved; false if the virtual
	 *            machine just wants to determine whether the class exists or
	 *            not
	 * @return the resulting Class.
	 * @exception ClassNotFoundException
	 *                if the class loader cannot find a the requested class.
	 */
	public synchronized Class loadClass(String name, boolean resolve)
			throws ClassNotFoundException {
		if (!NtbEnv.isUICall()) {
			throw new ClassNotFoundException(name);
		}

		//if (debug > 0)
//			log("@@@@@loadClass() " + name);
		// The class object that will be returned.
		Class c = null;

		// Use the cached value, if this class is already loaded into
		// this classloader.
		ClassCacheEntry classCache = (ClassCacheEntry) cache.get(name);

		if (classCache != null) {
			//if (debug > 0)
				log("Found in cache " + name);
			// Class found in our cache
			c = classCache.loadedClass;
			if (resolve)
				resolveClass(c);
			return c;
		}

		// Make sure we can access this class when using a SecurityManager
		if (sm != null) {
			int i = name.lastIndexOf('.');
			if (i >= 0) {
				String strPack =name.substring(0, i);
				log("$$$$$check package access permission: " + strPack);
				if(!debug)
					sm.checkPackageAccess(strPack);
//				sm.checkPackageDefinition(strPack);
			}
		}

		if (parent != null) {
			try {
				
				c = parent.loadClass(name);
				if (c != null) {
//					if (debug > 0)
						log("loadClass() from parent " + name);
					if (resolve)
						resolveClass(c);
					return c;
				}
			} catch (ClassNotFoundException e) {
				c = null;
			} catch (Exception e) {
				c = null;
			}
		}

		// Attempt to load the class from the system
		
		try {
			c = loadSystemClass(name, resolve);
			if (c != null) {
//				if (debug > 0)
				log("loadClass() from system " + name);
				return c;
			}
		} catch (Exception e) {
			c = null;
		}

		// Try to load it from Database
		try {
			NtbScript aScript = dataManager.getScriptByPK(name);
			if (aScript != null && aScript.getIscompiled() && aScript.getCompiled_obj() != null) {
				// Define the class with a ProtectionDomain if using
				// a SecurityManager
				// Origin is set by the specific loader
//				if (debug > 0)
				log("loadClass() from database " + name);
				//if (debug > 0)
					log("Before define class " + name);
				try {
					byte[] classData = aScript.getCompiled_obj().getByteCode();
					classCache = new ClassCacheEntry();
					classCache.loadedClass = doDefineClass(aScript.getObjname(), classData);
					classCache.className = name;
					classCache.lastModified = aScript.getTs().getMillis();
					//if (debug > 0)
						log("Class defined " + classCache.loadedClass);
					// Cache the result;

					// Resolve it if necessary
					if (resolve)
						resolveClass(classCache.loadedClass);
					//if (debug > 0)
						log("Add to cache() " + name + " " + classCache);
					cache.put(name, classCache);
				} catch (Throwable t) {
					NtbLogger.error(t);
				}
			
				return classCache.loadedClass;			
			}
		} catch (Exception ex) {
			NtbLogger.error(ex);
		}			
		// If not found in any repository
		throw new ClassNotFoundException(name);
	}

	// Override this with PD
	protected Class doDefineClass(String name, byte classData[]) {
		return defineClass(name, classData, 0, classData.length);
	}

	/**
	 * Load a class using the system classloader.
	 * 
	 * @exception ClassNotFoundException
	 *                if the class loader cannot find a the requested class.
	 * @exception NoClassDefFoundError
	 *                if the class loader cannot find a definition for the
	 *                class.
	 */
	private Class loadSystemClass(String name, boolean resolve)
			throws NoClassDefFoundError, ClassNotFoundException {
		Class c = findSystemClass(name);
		// Throws if not found.

		// // Add cache entry
		// ClassCacheEntry cacheEntry = new ClassCacheEntry();
		// cacheEntry.origin = null;
		// cacheEntry.loadedClass = c;
		// cacheEntry.lastModified = Long.MAX_VALUE;
		// cache.put(name, cacheEntry);

		if (resolve)
			resolveClass(c);

		return c;
	}

	public String toString() {
		return "NtbSecureScriptLoader(  )";
	}

	public static void setSm(SecurityManager sm) {
		NtbSecureScriptLoader.sm = sm;
	}

}
